/*
 * AllwinnerTech hdmi cec driver
 *
 * Copyright (C) 2015 AllwinnerTech Corporation
 *
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

#include <linux/device.h>
#include <linux/sched.h>
#include <linux/kthread.h>
#include <linux/err.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/cdev.h>
#include <linux/types.h>
#include <linux/workqueue.h>
#include <linux/module.h>
#if defined(CONFIG_HAS_EARLYSUSPEND)
#include <linux/earlysuspend.h>
#endif

#include "hdmi_cec.h"

#define DEBUG

int debug_mask = 0x00;
int _feature_enable = 0;
static struct platform_device *pdev;
static struct delayed_work hotplug_work;

extern int hdmi_hotplug_state(void);

static ssize_t hdmi_cec_show(struct class *class,
		struct class_attribute *attr, char *buf)
{
	int cnt = 0;
	int phy_addr;

	phy_addr = cec_get_physical_address(PHY_ADDR_TYPE_LOCAL);
	cnt += sprintf(buf, "local physical address: %d.%d.%d.%d\n",
			(phy_addr>>12)&0xf, (phy_addr>>8)&0xf,
			(phy_addr>> 4)&0xf, (phy_addr>>0)&0xf);

	phy_addr = cec_get_physical_address(PHY_ADDR_TYPE_ROOT);
	cnt += sprintf(buf+cnt, " root physical address: %d.%d.%d.%d\n",
			(phy_addr>>12)&0xf, (phy_addr>>8)&0xf,
			(phy_addr>> 4)&0xf, (phy_addr>>0)&0xf);

	return cnt;
}

static ssize_t hdmi_cec_store(struct class *class,
		struct class_attribute *attr, const char *buf, size_t count)
{
	char cmd = *buf;

	cec_debug("debug cmd: %c\n", cmd);
//	hdmi_cec_cmd_process(cmd);
	return count;
}

static ssize_t hdmi_channel_show(struct class *class,
		struct class_attribute *attr, char *buf)
{
	int address = 0;
	int cnt = 0;

	address = cec_get_physical_address(PHY_ADDR_TYPE_ACTIVE);
	cnt += sprintf(buf, "active: %d.%d.%d.%d\n",
			(address>>12)&0xf, (address>>8)&0xf,
			(address>> 4)&0xf, (address>>0)&0xf);

	address = cec_get_physical_address(PHY_ADDR_TYPE_LOCAL);
	cnt += sprintf(buf+cnt, " local: %d.%d.%d.%d\n",
			(address>>12)&0xf, (address>>8)&0xf,
			(address>> 4)&0xf, (address>>0)&0xf);

	return cnt;
}

static ssize_t hdmi_channel_store(struct class *class,
		struct class_attribute *attr, const char *buf, size_t count)
{
	char cmd = *buf;

	switch (cmd) {
		case '1':
//			hdmi_cec_enable_display();
			break;
		case '0':
//			hdmi_cec_disable_display();
			break;
		default:
			pr_err("unknow cmd '%c'\n", cmd);
			break;
	}

	return count;
}

static ssize_t hdmi_enable_show(struct class *class,
		struct class_attribute *attr, char *buf)
{
	return sprintf(buf, "%d\n", _feature_enable);
}

static ssize_t hdmi_enable_store(struct class *class,
		struct class_attribute *attr, const char *buf, size_t count)
{
	char cmd = *buf;

	if (cmd=='1')
		_feature_enable = 1;
	else
		_feature_enable = 0;

	return count;
}

static struct class_attribute hdmi_cec_class_attrs[] = {
	__ATTR(hdmi_cec,
	       S_IRUGO | S_IWUSR,
	       hdmi_cec_show,
	       hdmi_cec_store),

	__ATTR(hdmi_channel,
	       S_IRUGO | S_IWUSR,
	       hdmi_channel_show,
	       hdmi_channel_store),

	__ATTR(enable,
	       S_IRUGO | S_IWUSR,
	       hdmi_enable_show,
	       hdmi_enable_store),

	__ATTR_NULL
};

static struct class hdmi_cec_class = {
	.name = "hdmi-cec",
	.class_attrs = hdmi_cec_class_attrs,

};

#if defined(CONFIG_HAS_EARLYSUSPEND)
void hdmi_cec_early_suspend(struct early_suspend *h)
{
	if (_feature_enable) {
		cec_standby_broadcast();
		msleep(200);
	}
}

void hdmi_cec_late_resume(struct early_suspend *h)
{
	if (_feature_enable) {
		cec_wakeup_broadcast();
		msleep(200);
	}
}

static struct early_suspend hdmi_cec_early_suspend_handler =
{
	.level   = EARLY_SUSPEND_LEVEL_DISABLE_FB + 300,
	.suspend = hdmi_cec_early_suspend,
	.resume  = hdmi_cec_late_resume,
};

void register_early_suspend_callback(void)
{
	register_early_suspend(&hdmi_cec_early_suspend_handler);
}

void unregister_early_suspend_callback(void)
{
	unregister_early_suspend(&hdmi_cec_early_suspend_handler);
}
#else

void register_early_suspend_callback(void) { return; }
void unregister_early_suspend_callback(void) { return; }
#endif

static int _hotplug_state = -1;
void hdmi_hotplug_handler(struct work_struct *work)
{
	int state = hdmi_hotplug_state();

	cec_get_phy_addr_from_edid();
	if (_hotplug_state != state) {
		cec_debug("hdim port hotplug change\n");
		_hotplug_state = state;
		if (state) {
			pr_debug("hdmi cec process start...\n");
			msleep(200);
			if (hdmi_hotplug_state()) {
				if (!cec_get_phy_addr_from_edid()) {
					cec_debug("got cec physcial address from edid\n");
				}
				cec_polling_message();
			} else {
				cec_core_reset();
			}
		}
		else {
			cec_core_reset();
			pr_debug("hdmi cec process stop...\n");
		}
	}
	schedule_delayed_work(&hotplug_work, msecs_to_jiffies(1000));
}

/* Device model stuff */
static int hdmi_cec_probe(struct platform_device *dev)
{
	int ret;

	ret = cec_core_init();

	if (!ret) {
		register_early_suspend_callback();
		class_register(&hdmi_cec_class);
		printk(KERN_INFO "hdmi-cec: device successfully initialized.\n");
	}

	INIT_DELAYED_WORK(&hotplug_work, hdmi_hotplug_handler);
	schedule_delayed_work(&hotplug_work, msecs_to_jiffies(1000));

	return ret;
}

static int hdmi_cec_remove(struct platform_device *dev)
{
	cancel_delayed_work_sync(&hotplug_work);
	class_unregister(&hdmi_cec_class);
	unregister_early_suspend_callback();
	cec_core_exit();
	printk(KERN_INFO "hdmi-cec: device successfully initialized.\n");
	return 0;
}

#if 0
static int hdmi_cec_suspend(struct platform_device *dev, pm_message_t state)
{
	__disp_set_standby_to_tv();
	msleep(200);
	return 0;

}
static int hdmi_cec_resume(struct platform_device *dev)
{
	__disp_set_cec_image_view_on();
	msleep(200);
	return 0;
}
#endif


void hdmi_cec_shutdown(struct platform_device *dev)
{
	cec_standby_broadcast();
	msleep(200);
}

static struct platform_driver hdmi_cec_driver = {
	.probe   = hdmi_cec_probe,
	.remove  = hdmi_cec_remove,
	.shutdown = hdmi_cec_shutdown,
#if 0
	.suspend = hdmi_cec_suspend,
	.resume  = hdmi_cec_resume,
#endif
	.driver = {
		.name = "hdmi-cec",
		.owner = THIS_MODULE,
	},
};

/* Module stuff */
static int __init hdmi_cec_platform_init(void)
{
	int ret;

	ret = platform_driver_register(&hdmi_cec_driver);
	if (ret)
		goto out;

	pdev = platform_device_register_simple("hdmi-cec", -1, NULL, 0);
	if (IS_ERR(pdev)) {
		ret = PTR_ERR(pdev);
		goto out_driver;
	}

	printk(KERN_INFO "hdmi-cec: driver successfully loaded.\n");
	return 0;

out_driver:
	platform_driver_unregister(&hdmi_cec_driver);
out:
	printk(KERN_WARNING "hdmi-cec: driver init failed (ret=%d)!\n",
	       ret);
	return ret;
}

static void __exit hdmi_cec_platform_exit(void)
{
	platform_device_unregister(pdev);
	platform_driver_unregister(&hdmi_cec_driver);

	printk(KERN_INFO "hdmi-cec: driver unloaded.\n");
}

module_init(hdmi_cec_platform_init);
module_exit(hdmi_cec_platform_exit);
module_param_named(debug_mask, debug_mask, int, 0644);

MODULE_DESCRIPTION("AllwinnerTech hdmi cec driver");
MODULE_LICENSE("GPL");
